import * as shlib from '../shlib/browser'


/******************************************************************************
 * **简介**
 * 
 *      为描述方便，有以下快捷键相关定义：
 * 
 *      * 修饰键：包括左右 Ctrl 键、左右 Shift 键，左右 Alt 键，以及 Meta 键
 *      * 快捷键主键：用户按下一个或多个修饰键后不抬起，继续按下的第一个键。取值范围如
 *        下：
 *          * 'A' - 'Z'
 *          * '0' - '9'
 *          * 'F1' - 'F12'
 *          * '`', '-', '=', '[', ']', '\', ';', '\'', ',', '.', '/', 'Tab',
 *          * 'Insert', 'Delete', 'Home', 'End', 'PgUp', 'PgDn', 'Enter'
 *          * 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'
 *      * 快捷键子健：用户在按下主键（并抬起）后，在不抬起修饰键的情况下紧接着按下的第
 *        二个键。取值范围同主键。快捷键子键是可选的。
 * 
 * **快捷键表达式（Hot Key Expression）**
 * 
 *      快捷键表达式是快捷键的易于阅读的标准字符串表示形式，具有唯一性，可作为字典索引
 *      来查找一个快捷键（ShHotKey）对象。其形式如下：
 * 
 *      ["Ctrl+"]["Shift+"]["Alt+"]["Meta+"]<key1>[","<key2>]
 * 
 *      其中：
 *      * 前面的四个修饰键必须出现至少一个，并且排列顺序不能变化；
 *      * <key1> 是快捷键主键，可选的 <key2> 是快捷键子健；
 * 
 * **用法**
 * 
 *      第一步：在应用程序的合适位置完成对快捷键功能的初始化：
 *      * 创建多个快捷键对象，并调用 register() 方法登记该快捷键
 *      * 调用 ShHotKey.setup() 静态方法，为 window 对象增加最顶层的 keydown 事件处
 *      理程序，用于识别用户按键是否为快捷键
 * 
 *      第二步：编写 window 对象的 'shev-hotkey' 事件处理器，并调用
 *      window.addEventListener() 添加之。
 * 
 */

/**
 * 快捷键的类定义。
 */
export class ShHotKey {
    /**
     * 从 KeyboardEvent.code 到按键的显示名字的转换表。
     */
    private static _hotKeyValueMap: Record<string, string> = {
        KeyA: 'A',
        KeyB: 'B',
        KeyC: 'C',
        KeyD: 'D',
        KeyE: 'E',
        KeyF: 'F',
        KeyG: 'G',
        KeyH: 'H',
        KeyI: 'I',
        KeyJ: 'J',
        KeyK: 'K',
        KeyL: 'L',
        KeyM: 'M',
        KeyN: 'N',
        KeyO: 'O',
        KeyP: 'P',
        KeyQ: 'Q',
        KeyR: 'R',
        KeyS: 'S',
        KeyT: 'T',
        KeyU: 'U',
        KeyV: 'V',
        KeyW: 'W',
        KeyX: 'X',
        KeyY: 'Y',
        KeyZ: 'Z',

        Digit1: '1',
        Digit2: '2',
        Digit3: '3',
        Digit4: '4',
        Digit5: '5',
        Digit6: '6',
        Digit7: '7',
        Digit8: '8',
        Digit9: '9',
        Digit0: '0',

        F1: 'F1',
        F2: 'F2',
        F3: 'F3',
        F4: 'F4',
        F5: 'F5',
        F6: 'F6',
        F7: 'F7',
        F8: 'F8',
        F9: 'F9',
        F10: 'F10',
        F11: 'F11',
        F12: 'F12',

        Backquote: '`',
        Minus: '-',
        Equal: '=',
        BracketLeft: '[',
        BracketRight: ']',
        Backslash: '\\',
        Semicolon: ';',
        Quote: '\'',
        Comma: ',',
        Period: '.',
        Slash: '/',

        Space: 'Space',
        Tab: 'Tab',
        Backspace: 'Backspace',
        Enter: 'Enter',
        Insert: 'Insert',
        Delete: 'Delete',
        PageUp: 'PageUp',
        PageDown: 'PageDown',
        Home: 'Home',
        End: 'End',

        ArrowLeft: 'ArrowLeft',
        ArrowRight: 'ArrowRight',
        ArrowUp: 'ArrowUp',
        ArrowDown: 'ArrowDown',
    }
    private static _hotKeyValues = Object.values(ShHotKey._hotKeyValueMap)

    /**
     * 应用程序注册的所有快捷键列表。修饰键和主键相同而子键不同的快捷键被合并
     * 了。
     */
    private static _hotKeys: ShHotKey[] = []

    /**
     * 当用户同时按下修饰键和主键后，如果在快捷键列表中找到对应项目且找到的快捷
     * 键是有子键的，就先保存下来。
     */
    private static _tempHotKey: ShHotKey | null = null

    /**
     * 安装事件处理器。
     */
    public static setup() {
        window.addEventListener('keydown', (ev: KeyboardEvent) => {
            // 如果修饰键一个都没按下，则放过
            if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
                ShHotKey._tempHotKey = null
                return
            }

            // 如果按键不是一个合法的快捷键按键，则放过
            const kv = ShHotKey._hotKeyValueMap[ev.code]
            if (!kv) {
                ShHotKey._tempHotKey = null
                return
            }

            // 如果刚刚按下过修饰键和主键，此时修饰键应该和之前的修饰键相同，且 kv 是子键
            if (ShHotKey._tempHotKey) {
                const tk = ShHotKey._tempHotKey
                // 如果修饰键状态和之前按主键时相同，同时 kv 也匹配上期待的子键
                if (tk.matchModifiers(ev) && tk._subKeys.indexOf(kv) >= 0) {
                    window.dispatchEvent(new ShHotKeyEvent(new ShHotKey(tk._ctrl, tk._shift, tk._alt, tk._meta, tk._key, [kv,])))
                    ev.preventDefault()
                    ev.stopImmediatePropagation()
                }
            }
            // 如果没有按下过修饰键和主键，此时 kv 应该是主键
            else {
                for (let i = 0; i < ShHotKey._hotKeys.length; i++) {
                    const hk = ShHotKey._hotKeys[i]
                    // 在注册的快捷键列表中找到修饰键和主键都匹配当前按键状态的条目
                    if (hk.matchModifiers(ev) && hk._key == kv) {
                        if (hk._subKeys.length > 0) { // 如果需要等待子键
                            ShHotKey._tempHotKey = hk
                        } else { // 如果不需要子键
                            window.dispatchEvent(new ShHotKeyEvent(hk))
                        }
                        ev.preventDefault()
                        ev.stopImmediatePropagation()
                        return
                    }
                }
            }
        }, true)
    }

    /**
     * 通过解析快捷键表达式来生成 ShHotKey 对象。
     * 
     * @param hotKeyExpr 快捷键表达式
     */
    public static fromHotKeyExpr(hotKeyExpr: string): ShHotKey {

        let ctrl: boolean = false
        let shift: boolean = false
        let alt: boolean = false
        let meta: boolean = false
        let masterKey: string = ''
        let subKeys: string[] = []

        let keys = hotKeyExpr.split('+')
        shlib.assert(keys.length >= 2)

        // 解析修饰键状态
        for (let i = 0; i < keys.length - 1; i++) {
            const k = keys[i].trim().toUpperCase()
            ctrl = ctrl || k === 'CTRL'
            shift = shift || k === 'SHIFT'
            alt = alt || k === 'ALT'
            meta = meta || k === 'META'
        }
        // 四个修饰键中必须按下至少一个
        shlib.assert(ctrl || shift || alt || meta)

        keys = keys[keys.length - 1].split(',', 2)

        // 解析主键
        masterKey = keys[0]
        shlib.assert(ShHotKey._hotKeyValues.indexOf(masterKey) >= 0)

        // 解析子键
        if (keys.length === 2) {
            subKeys.push(keys[1])
            shlib.assert(ShHotKey._hotKeyValues.indexOf(subKeys[0]) >= 0)
        }

        return new ShHotKey(ctrl, shift, alt, meta, masterKey, subKeys)
    }

    /**
     * 在全局注册快捷键列表中新增一个快捷键。
     * 
     * @param hotKeyExpr 要注册的快捷键的表达式
     */
    public static registerHotKey(hotKeyExpr: string) {
        const thisKey = ShHotKey.fromHotKeyExpr(hotKeyExpr)

        for (let i = 0; i < ShHotKey._hotKeys.length; i++) {
            const hk = ShHotKey._hotKeys[i]

            // 在注册快捷键列表中找到一条修饰键和主键都匹配的条目
            if (thisKey.matchModifiers(hk) && thisKey._key == hk._key) {
                // 把自己的子键追加上去
                if (thisKey._subKeys.length > 0)
                    hk._subKeys.push(thisKey._subKeys[0])

                return
            }
        }

        // 现在的注册快捷键列表中没有修饰键和主键都匹配的条目，则增加条目
        ShHotKey._hotKeys.push(thisKey)
    }

    /**
     * 分别指示四种修饰键是否被按下。
     */
    private _ctrl: boolean = false
    private _shift: boolean = false
    private _alt: boolean = false
    private _meta: boolean = false

    /**
     * 快捷键主键的键值。
     */
    private _key: string = ''

    /**
     * 快捷键子键的键值列表。在表示一个快捷键的时候，只能有一个值。但在
     * 注册快捷键列表中，为了方便，会将修饰键和主键相同的那些快捷键合并，
     * 不同的子键放在该列表中。
     */
    private _subKeys: string[] = []

    /**
     * 构造函数。
     */
    constructor(ctrl: boolean, shift: boolean, alt: boolean, meta: boolean, masterKey: string, subKeys: string[]) {
        this._ctrl = ctrl
        this._shift = shift
        this._alt = alt
        this._meta = meta
        this._key = masterKey
        this._subKeys = subKeys
    }

    /**
     * 返回快捷键表达式字符串。
     */
    public toString(): string {
        const s = []
        if (this._ctrl) s.push('Ctrl+')
        if (this._shift) s.push('Shift+')
        if (this._alt) s.push('Alt+')
        if (this._meta) s.push('Meta+')
        s.push(this._key)
        if (this._subKeys.length > 0) {
            s.push(',')
            s.push(this._subKeys[0])
        }

        return s.join('')
    }

    /**
     * 判断键盘事件的修饰键状态是否和自己相同。
     * 
     * @param ke 要比较的键盘事件
     * @returns 修饰键状态是否相同
     */
    private matchModifiers(ke: KeyboardEvent | ShHotKey): boolean {
        if (ke instanceof KeyboardEvent)
            return this._ctrl == ke.ctrlKey && this._shift == ke.shiftKey && this._alt == ke.altKey && this._meta == ke.metaKey
        else
            return this._ctrl == ke._ctrl && this._shift == ke._shift && this._alt == ke._alt && this._meta == ke._meta
    }
}


declare global {
    interface WindowEventMap {
        'shev-hotkey': ShHotKeyEvent,
    }
}

export class ShHotKeyEvent extends Event {

    public hotKey: ShHotKey

    constructor(hotKey: ShHotKey) {
        super('shev-hotkey')
        this.hotKey = hotKey
    }
}

